
/**
 * Copyright 2009 cestum.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
package org.waveprotocol.wave.model.util {
import org.waveprotocol.wave.model.util.codePoint.GoodUTF16ForBlip;
import org.waveprotocol.wave.model.util.codePoint.GoodUTF16ForDataDoc;
import org.waveprotocol.wave.model.util.codePoint.UnpairedSurrogates;
import org.waveprotocol.wave.model.util.codePoint.ValidUTF16;
import org.waveprotocol.wave.model.util.codePoint.XMLName;

/**
 * TODO optimize it
 **/
public final class Utf16Util {
	
	function Utf16Util() {}
	public static var REPLACEMENT_CHARACTER:Number= 0xFFFD;
	
	//  private static const var COLON:Number = String.fromCharCode(":");
	//  private static const var COLON:Number = String.fromCharCode(";");
	//  private static const var COLON:Number = String.fromCharCode("A:");
	//  private static const var COLON:Number = String.fromCharCode("Z");
	//  private static const var COLON:Number = String.fromCharCode(":");
	
	
	public static function isCodePoint(c:String):Boolean {
		var n:Number = c.charCodeAt(0);
		return 0<= n && n <= 0x10ffff;
	}
	public static function isSurrogate_char(c:String):Boolean {
		var n:Number = c.charCodeAt(0);
		return 0xd800<= n && n <= 0xdfff;
	}
	
	
	//  public static function isSurrogate_String(c:String):Boolean {
	//    return 0xd800<= c && c <= 0xd;
	//  }
	
	public static function isLowSurrogate(c:String):Boolean {
		var n:Number = c.charCodeAt(0);
		return 0xdc00<= n && n <= 0xdfff;
	}
	
	public static function isHighSurrogate(c:String):Boolean {
		var n:Number = c.charCodeAt(0);
		return 0xd800<= n && n <= 0xdbff;
	}
	
	//  public static function isSurrogate_int(c:int):Boolean {
	//    if (!isCodePoint(c)) {
	//      throw new Error("isSurrogate_int: Not a code point: 0x");
	//    }
	//    return 0xd800<= c && c <= 0xd;
	//  }
	
	public static function isSupplementaryCodePoint(c:String):Boolean {
		if (!isCodePoint(c)) {
			throw new Error("isSupplementaryCodePoint: Not a code point: 0x");
		}
		return c.charCodeAt(0) >= 0x10000;
	}
	
	/**
	 * Traverses the given UTF-16 string from left to right, decoding surrogates
	 * into code points, and calls the handler for each code point and unmatched
	 * surrogate.
	 *
	 * The return values of the handler's methods determine whether
	 * to continue traversal and the return value of traverseUtf16String.
	 *
	 * Traversal continues as long as the handler returns null.  If handler
	 * returns a non-null value, traversal immediately terminates, and
	 * traverseUtf16String returns the value the hander returned.
	 * If the end of the string is reached, traverseUtf16String calls
	 * handler.endOfString() and returns its value.
	 */
	public static function traverseUtf16String(s:String, handler:CodePointHandler):Boolean {
		//FIXME Definitly needs to be optimized...
		Preconditions.checkNotNull(s, "Null string");
		nextCodeUnit:
		for (var i:int= 0; i < s.length; i++) {
			var cp:String;
			var c:String= s.charAt(i);
			if (isSurrogate_char(c)) {
				if (isLowSurrogate(c)) {
					// unexpected trailing (low) surrogate
					var v1:Boolean= Boolean(handler.unpairedSurrogate(c));
					if (v1 != null) {
						return v1;
					}
					continue nextCodeUnit;
				}
				// leading (high) surrogate
				i++;
				if (i >= s.length) {
					var v2:Boolean= Boolean(handler.unpairedSurrogate(c));
					if (v2 != null) {
						return v2;
					}
					break nextCodeUnit;
				}
				var c2:String= s.charAt(i);
				if (isLowSurrogate(c2)) {
					// low surrogate as expected
					cp = String.fromCharCode(((c.charCodeAt(0) - 0xD800) << 10) + (c2.charCodeAt(0) - 0xDC00) + 0x010000);
				} else {
					// either not a surrogate, or a high surrogate
					var v3:Boolean= Boolean(handler.unpairedSurrogate(c));
					if (v3 != null) {
						return v3;
					}
					i--;
					continue nextCodeUnit;
				}
			} else {
				cp = String.fromCharCode(c.charCodeAt(0));
			}
			var v4:Object = handler.codePoint(cp);
			if (v4 != null) {
				return Boolean(v4);
			}
		}
		return Boolean(handler.endOfString());
	}
	
	
	
	
	/**
	 * Traverses the given UTF-16 string from left to right, decoding surrogates
	 * into code points, and calls the handler for each code point and unmatched
	 * surrogate.
	 *
	 * The return values of the handler's methods determine whether
	 * to continue traversal and the return value of traverseUtf16String.
	 *
	 * Traversal continues as long as the handler returns null.  If handler
	 * returns a non-null value, traversal immediately terminates, and
	 * traverseUtf16String returns the value the hander returned.
	 * If the end of the string is reached, traverseUtf16String calls
	 * handler.endOfString() and returns its value.
	 * @return 
	 */
	public static function voidTraverseUtf16String(s:String, handler:CodePointHandler):void {
		Preconditions.checkNotNull(s, "Null string");
		nextCodeUnit:
		for (var i:int= 0; i < s.length; i++) {
			var cp:String;
			var c:String= s.charAt(i);
			if (isSurrogate_char(c)) {
				if (isLowSurrogate(c)) {
					// unexpected trailing (low) surrogate
					handler.unpairedSurrogate(c);
					continue nextCodeUnit;
				}
				// leading (high) surrogate
				i++;
				if (i >= s.length) {
					handler.unpairedSurrogate(c);
					break nextCodeUnit;
				}
				var c2:String= s.charAt(i);
				if (isLowSurrogate(c2)) {
					// low surrogate as expected
					// cp = Character.toCodePoint(c, c2);
					cp = String.fromCharCode(((c.charCodeAt(0) - 0xD800) << 10) + (c2.charCodeAt(0) - 0xDC00) + 0x010000);
				} else {
					// either not a surrogate, or a high surrogate
					handler.unpairedSurrogate(c);
					i--;
					continue nextCodeUnit;
				}
			} else {
				cp = String.fromCharCode(c.charCodeAt(0));
			}
			handler.codePoint(cp);
		}
		//    return handler.endOfString();
	}
	
	/**
	 * Returns the index of the first surrogate character in the given string,
	 * or -1 if there aren't any.
	 *
	 * Does not check whether surrogates in s are paired correctly.
	 */
	public static function firstSurrogate(s:String):int {
		for (var i:int= 0; i < s.length; i++) {
			if (isSurrogate_char(s.charAt(i))) {
				return i;
			}
		}
		return -1;
	}
	
	/**
	 * Returns the index of the first non-surrogate character in the given string,
	 * or -1 if all characters in s are surrogates.
	 *
	 * Does not check whether surrogates in s are paired correctly.
	 */
	public static function firstNonSurrogate(s:String):int {
		for (var i:int= 0; i < s.length; i++) {
			if (!isSurrogate_char(s.charAt(i))) {
				return i;
			}
		}
		return -1;
	}
	
	
	public static function isCodePointValid(c:String):Boolean {
		if (!isCodePoint(c)) {
			Preconditions.illegalArgument("Not a code point: 0x");
		}
		if (isSurrogate_char(c)) {
			Preconditions.illegalArgument("Code point is a surrogate: 0x");
		}
		
		// noncharacters
		{
			var d:int= c.charCodeAt(0) & 0xFFFF;
			if (d == 0xFFFE|| d == 0xFFFF) { return false; }
		}
		if (0xFDD0<= c.charCodeAt(0) && c.charCodeAt(0) <= 0xFDEF) { return false; }
		return true;
	}
	
	/**
	 * Returns whether the given code point is acceptable for blip content.
	 *
	 * This definition is based on RFC5198 (section 2 in particular)
	 * and a few internal discussions.
	 *
	 * It may turn out to be overly restrictive, but relaxing it in the
	 * future is easy.
	 */
	public static function isCodePointGoodForBlip(c1:String):Boolean {
		Preconditions.checkArgument(isCodePoint(c1),
			"Not a code point: 0x");
		Preconditions.checkArgument(!isSurrogate_char(c1),
			"Code point is a surrogate: 0x");
		if (!isCodePointValid(c1)) { return false; }
		var c:Number = c1.charCodeAt(0);
		// control codes
		if (0<= c && c <= 0x1|| 0x7<= c && c <= 0x9) { return false; }
		// private use
		// we permit these, they can be used for things like emoji
		//if (0xE000 <= c && c <= 0xF8FF) { return false; }
		//if (0xF0000 <= c && c <= 0xFFFFD) { return false; }
		//if (0x100000 <= c && c <= 0x10FFFD) { return false; }
		// deprecated format characters
		if (0x206A<= c && c <= 0x206F) { return false; }
		// TODO: investigate whether we can lift some of these restrictions
		// bidi markers
		if (c == 0x200E|| c == 0x200F) { return false; }
		if (0x202A<= c && c <= 0x202E) { return false; }
		// tag characters, strongly discouraged
		if (0xE0000<= c && c <= 0xE007F) { return false; }
		return true;
	}
	
	/**
	 * Returns whether the given code point is acceptable for data document
	 * content.
	 *
	 * For now, it allows any valid Unicode.
	 */
	public static function isCodePointGoodForDataDocument(c:String):Boolean {
		Preconditions.checkArgument(isCodePoint(c),
			"Not a code point: 0x");
		Preconditions.checkArgument(!isSurrogate_char(c),
			"Code point is a surrogate: 0x");
		if (!isCodePointValid(c)) { return false; }
		return true;
	}
	
	/**
	 * Returns whether a given code point is a NameStartChar according to XML
	 * and valid Unicode.
	 *
	 * See http://www.w3.org/TR/xml/#NT-NameStartChar and isCodePointValid().
	 */
	public static function isXmlNameStartChar(c1:String):Boolean {
		// There are some obvious ways to speed this up, but let's not bother until
		// profiles show that it matters.
		
		// NameStartChar ::= ":" | [A-Z] | "_" | [a-z]
		var c = c1.charCodeAt(0);
		return c1 == ':' || ('A' <= c1 && c1 <= 'Z') || c1 == '_' || ('a' <= c1 && c1 <= 'z')
			//             | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF]
			|| (0xC0<= c && c <= 0xD6) || (0xD8<= c && c <= 0xF6) || (0xF8<= c && c <= 0x2FF)
			//             | [#x370-#x37D] | [#x37F-#x1FFF]
			|| (0x370<= c && c <= 0x37D) || (0x37F<= c && c <= 0x1FFF)
			//             | [#x200C-#x200D] | [#x2070-#x218F]
			|| (0x200C<= c && c <= 0x200D) || (0x2070<= c && c <= 0x218F)
			//             | [#x2C00-#x2FEF] | [#x3001-#xD7FF]
			|| (0x2C00<= c && c <= 0x2FEF) || (0x3001<= c && c <= 0xD7FF)
			//             | [#xF900-#xFDCF] | [#xFDF0-#xFFFD]
			|| (0xF900<= c && c <= 0xFDCF) || (0xFDF0<= c && c <= 0xFFFD)
			//             | [#x10000-#xEFFFF]
			|| ((0x10000<= c && c <= 0xEFFFF) && isCodePointValid(c));
	}
	
	/**
	 * Returns whether a given code point is a NameChar according to XML
	 * and valid Unicode.
	 *
	 * See http://www.w3.org/TR/xml/#NT-NameChar and isCodePointValid().
	 */
	public static function isXmlNameChar(c1:String):Boolean {
		// There are some obvious ways to speed this up, but let's not bother until
		// profiles show that it matters.
		
		if (!isCodePointValid(c1)) { return false; }
		
		// NameChar ::= NameStartChar | "-" | "." | [0-9]
		var c:Number = c1.charCodeAt(0);
		return isXmlNameStartChar(c1) || c1 == '-' || c1 == '.' || ('0' <= c1 && c1 <= '9')
			//          | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
			|| c == 0xB7|| (0x0300<= c && c <= 0x036F) || (0x203F<= c && c <= 0x2040);
	}
	
	
	/**
	 * Returns whether a given string is a Name according to XML and valid
	 * Unicode.
	 *
	 * See http://www.w3.org/TR/xml/#NT-Name and isValidUtf16().
	 */
	public static function isXmlName(s:String):Boolean {
		// Name ::= NameStartChar (NameChar)*
		Preconditions.checkNotNull(s, "Null XML name string");
		if (StrUtil.isEmpty(s)) {
			return false;
		}
		return traverseUtf16String(s, new XMLName());
	}
	
	
	private static var UNPAIRED_SURROGATES:CodePointHandler= new UnpairedSurrogates();
	private static var VALID_UTF16:CodePointHandler= new ValidUTF16();
	private static var GOOD_UTF16_FOR_BLIP:CodePointHandler= new GoodUTF16ForBlip();
	private static var GOOD_UTF16_FOR_DATA_DOCUMENT:CodePointHandler= new GoodUTF16ForDataDoc();
	
	
	// Deprecated because I think this check is too weak to be useful.  We should
	// always also check for noncharacter codepoints etc.
	public static function containsUnpairedSurrogates(s:String):Boolean {
		return traverseUtf16String(s, UNPAIRED_SURROGATES);
	}
	
	public static function isValidUtf16(s:String):Boolean {
		return traverseUtf16String(s, VALID_UTF16);
	}
	
	public static function isGoodUtf16ForBlip(s:String):Boolean {
		return traverseUtf16String(s, GOOD_UTF16_FOR_BLIP);
	}  
	
	public static function isGoodUtf16ForDataDocument(s:String):Boolean {
		return traverseUtf16String(s, GOOD_UTF16_FOR_DATA_DOCUMENT);
	}
	
}
}